﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Shell;
using Newtonsoft.Json;
using Expression = System.Linq.Expressions.Expression;

namespace SmartNavigation
{
    public class SmartNavigationService
    {
        #region Singleton

        private SmartNavigationService()
        {
        }

        private static readonly object Lock = new object();

        private static volatile SmartNavigationService _current;

        public static SmartNavigationService Current
        {
            get
            {
                if (_current == null)
                {
                    lock (Lock)
                    {
                        if (_current == null)
                            _current = new SmartNavigationService();
                    }
                }
                return _current;
            }
        }

        #endregion Singleton

        private static MethodCallArgument[] GetArguments(MethodCallExpression callExpression)
        {
            var list = new List<MethodCallArgument>();

            foreach (var expression in callExpression.Arguments)
            {
                var unaryExpressions = Expression.Convert(expression, typeof(object));
                var item = Expression.Lambda<Func<object>>(unaryExpressions).Compile()();
                list.Add(new MethodCallArgument(item) { Type = expression.Type });
            }

            return list.ToArray();
        }

        private static MethodInfo GetMethodMarkedWithAttribute<TAttributeType>(Type type) where TAttributeType : Attribute
        {
            return type.GetMethods().FirstOrDefault(i => i.GetCustomAttributes(true).OfType<TAttributeType>().FirstOrDefault() != null);
        }

        private static void InvokeMethodMarkedWithAttribute<TAttributeType>(object target, object argument) where TAttributeType : Attribute
        {
            var methodMarkedWithAttribute = GetMethodMarkedWithAttribute<TAttributeType>(target.GetType());

            if (methodMarkedWithAttribute == null)
                return;

            var parameterInfos = methodMarkedWithAttribute.GetParameters();
            if (parameterInfos.Length > 1)
                throw new Exception("Method marked with OnNavigatedToAttribute has wrong signature.");

            if (parameterInfos.Length > 0 && parameterInfos[0].ParameterType != argument.GetType())
                throw new Exception("Method marked with OnNavigatedToAttribute has wrong signature.");


            if (parameterInfos.Length == 0)
                methodMarkedWithAttribute.Invoke(target, null);
            else
                methodMarkedWithAttribute.Invoke(target, new[] { argument });
        }

        private void InitFrameInner(Frame frame)
        {
            if (frame == null)
                return;

            if (_navigationFrame == null)
            {
                PhoneApplicationService.Current.Launching += ApplicationOnLaunching;
                _navigationFrame = frame;
                if (_navigationFrame != null)
                {
                    _navigationFrame.Navigated += NavigationFrameOnNavigated;
                    _navigationFrame.Navigating += NavigationFrameOnNavigating;
                }
            }
        }

        private void NavigationFrameOnNavigating(object sender, NavigatingCancelEventArgs e)
        {
            var page = NavigationFrame.Content as Page;

            if (page == null)
                return;

            if (e.IsCancelable == false && e.IsNavigationInitiator == false && e.NavigationMode == NavigationMode.Back)
                return;

            if (page.DataContext != null)
            {
                InvokeMethodMarkedWithAttribute<OnNavigatingFromAttribute>(page.DataContext, e);
            }
        }

        private void ApplicationOnLaunching(object sender, LaunchingEventArgs e)
        {
            StorageUtils.Storage.DeleteFolder(EntriesStoragePath);
        }

        private void NavigationFrameOnNavigated(object sender, NavigationEventArgs e)
        {
            var matchCollection = Regex.Matches(e.Uri.OriginalString, @"(?<key>[\w+\-()]*)\s*=(?<value>[\w+\-()]*)", RegexOptions.Singleline | RegexOptions.IgnoreCase);

            string entryId = null;

            foreach (Match match in matchCollection)
            {
                if (!match.Success)
                    continue;

                var key = match.Groups["key"].Value;
                if (key == UriKey)
                {
                    entryId = match.Groups["value"].Value;
                }
            }

            if (string.IsNullOrEmpty(entryId))
                return;

            NavigationEntry entry;
            if (!_navigationEntries.TryGetValue(entryId, out entry))
            {
                var storage = IsolatedStorageFile.GetUserStoreForApplication();

                var filePath = string.Format("{0}/{1}", EntriesStoragePath, entryId);


                if (storage.FileExists(filePath))
                {
                    using (var stream = new StreamReader(storage.OpenFile(filePath, FileMode.Open)))
                    {
                        entry = JsonConvert.DeserializeObject<NavigationEntry>(stream.ReadToEnd());
                    }
                }
            }

            if (entry == null)
                return;

            var page = e.Content as FrameworkElement;

            if (page == null)
                return;

            object dataContext = null;

            if (page.DataContext != null)
            {
                dataContext = page.DataContext;
            }
            else
            {
                var dataContextType = Type.GetType(entry.AssemblyQualifiedName);
                if (dataContextType == null)
                    return;

                dataContext = Activator.CreateInstance(dataContextType);
                page.DataContext = dataContext;

                var parameters = new List<object>();

                foreach (var argument in entry.Arguments)
                {
                    if (argument.Type == typeof(NavigationEventArgs))
                    {
                        parameters.Add(e);
                    }
                    else
                    {
                        parameters.Add(argument.GetValue());
                    }
                }

                dataContextType.GetMethod(entry.MethodName).Invoke(dataContext, parameters.ToArray());
            }

            InvokeMethodMarkedWithAttribute<OnNavigatedToAttribute>(dataContext, e);
        }

        private string _entriesStoragePath = "/NavigationEntries";
        private readonly Dictionary<string, NavigationEntry> _navigationEntries = new Dictionary<string, NavigationEntry>();
        private Frame _navigationFrame;

        private string _uriKey = "entryId";



        public string EntriesStoragePath
        {
            get { return _entriesStoragePath; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new ArgumentException("EntriesStoragePath must be a non-empty string");
                }
                _entriesStoragePath = value;
            }
        }

        public string UriKey
        {
            get { return _uriKey; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new ArgumentException("UriKey must be a non-empty string");
                }
                _uriKey = value;
            }
        }

        public Frame NavigationFrame
        {
            get
            {
                InitFrameInner(Application.Current.RootVisual as Frame);
                return _navigationFrame;
            }
        }

        public void Navigate<TDataContext>(string pageUri, Expression<Action<TDataContext>> arguments)
        {
            var methodCallExpression = arguments.Body as MethodCallExpression;

            if (methodCallExpression == null || methodCallExpression.Object == null)
                throw new NavigateException("Arguments expression has errors");

            var argumentValues = GetArguments(methodCallExpression);


            var guid = Guid.NewGuid().ToString();
            var navigationEntry = new NavigationEntry
                {
                    Id = guid,
                    MethodName = methodCallExpression.Method.Name,
                    Arguments = argumentValues,
                    AssemblyQualifiedName = methodCallExpression.Object.Type.AssemblyQualifiedName
                };


            _navigationEntries.Add(guid, navigationEntry);

            var serializedNavigationEntry = JsonConvert.SerializeObject(navigationEntry);

            StorageUtils.CreateDirectoryIfNotExists(EntriesStoragePath);
            var storage = StorageUtils.Storage;

            using (var stream = new StreamWriter(storage.OpenFile(string.Format("{0}/{1}", EntriesStoragePath, guid), FileMode.CreateNew)))
            {
                stream.Write(serializedNavigationEntry);
            }

            NavigationFrame.Navigate(new Uri(string.Format("{0}?{1}={2}", pageUri, UriKey, guid), UriKind.Relative));
        }

        public void InitFrame(Frame frame)
        {
            InitFrameInner(frame);
        }
    }
}